﻿// ———————————————————————–
// <copyright file="NonXMLContentType.cs" company="The MITRE Corporation">
//    Copyright (c) 2010 The MITRE Corporation. All rights reserved.
//    Licensed under the Apache License, Version 2.0 (the "License");
//    you may not use this file except in compliance with the License.
//    You may obtain a copy of the License at
//    http://www.apache.org/licenses/LICENSE-2.0
//    Unless required by applicable law or agreed to in writing, software
//    distributed under the License is distributed on an "AS IS" BASIS,
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//    See the License for the specific language governing permissions and
//    limitations under the License.
// </copyright>
// ———————————————————————–

/////////////////////////////////////////////////////////////////////////////////////////////////
// NonXMLContentType.cs - Container to hold Non-XML Content within a ContentObject
// Project: IC.NET - EDXL Library
//
// Language:    C#, .NET 4.0
// Platform:    Windows 7, VS 2010 Beta 2
// Author:      Don McGarry MITRE Rome

/////////////////////////////////////////////////////////////////////////////////////////////////

// Copyright (c) 2010 The MITRE Corporation. All rights reserved.
//
// NOTICE
// This software was produced for the U. S. Government
// under Contract No. FA8721-09-C-0001, and is
// subject to the Rights in Noncommercial Computer Software
// and Noncommercial Computer Software Documentation Clause
// (DFARS) 252.227-7014 (JUN 1995)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Security.Cryptography;
using System.ServiceModel.Syndication;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace EDXLSharp.EDXLDELib
{
  /// <summary>
  /// Container for content provided in a non-XML MIME type. 
  /// </summary>
  [Serializable]
  public partial class NonXMLContentType : IDEComposableObject, IGeoRSS
  {
    #region Private Member Variables

    /// <summary>
    /// The format of the payload. 
    /// </summary>
    private string mimeType;

    /// <summary>
    /// The file size of the payload . 
    /// </summary>
    private int? size;

    /// <summary>
    /// The digest value for the payload. 
    /// </summary>
    private string digest;

    /// <summary>
    /// A Uniform Resource Identifier that can be used to retrieve the identified resource. 
    /// </summary>
    private Uri uri;

    /// <summary>
    /// The base-64 encoded data content. 
    /// </summary>
    private byte[] binaryContentData;

    /// <summary>
    /// The geographic information for this non XML content
    /// </summary>
    private SyndicationElementExtension geoInfo;

    #endregion

    #region Constructors
    /// <summary>
    /// Initializes a new instance of the NonXMLContentType class
    /// Default Ctor - Does Nothing
    /// </summary>
    public NonXMLContentType()
    {
    }
    #endregion

    #region Public Accessors
    /// <summary>
    /// Gets or sets
    ///  MIME content type and sub-type as described in [RFC 2046].
    /// </summary>
    [XmlElement(ElementName = "mimeType", Order = 0)]
    public string MIMEType
    {
      get { return this.mimeType; }
      set { this.mimeType = value; }
    }

    /// <summary>
    /// Gets or sets
    ///  Value must be in bytes and represent the raw file size (not encoded or encrypted).
    /// </summary>
    [XmlElement(ElementName = "size", Order = 1)]
    public int? Size
    {
      get { return this.size; }
      set { this.size = value; }
    }

    /// <summary>
    /// Gets or sets
    ///  Used to ensure the integrity of the payload.
    ///  2. Calculated using the Secure Hash Algorithm (SHA-1)
    ///  Represented as a Base64 String
    /// </summary>
    [XmlElement(ElementName = "digest", Order = 2)]
    public string Digest
    {
      get
      {
        return this.digest;
      }

      set
      {
        this.CheckDigest(value);
        this.digest = value;
      }
    }

    /// <summary>
    /// Gets or sets
    ///  May be a full absolute URI, typically a Uniform Resource Locator, that can be used to retrieve
    ///  the resource over the Internet.
    /// </summary>
    [XmlIgnore]
    public Uri URI
    {
      get { return this.uri; }
      set { this.uri = value; }
    }

    /// <summary>
    /// Gets or sets
    /// The Serializable String Version of the URI
    /// </summary>
    [XmlAttribute("uri")]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public string UriString
    {
      get { return this.uri == null ? null : this.uri.ToString(); }
      set { this.uri = value == null ? null : new Uri(value); }
    }

    /// <summary>
    /// Gets or sets
    /// MAY be used either with or instead of the uri element in contexts where retrieval of a
    /// resource via a URI is not feasible.
    /// </summary>
    [XmlElement(ElementName = "contentData", Order = 4)]
    public string ContentData
    {
      get
      {
        if (this.binaryContentData != null)
        {
          return Convert.ToBase64String(this.binaryContentData);
        }
        else
        {
          return null;
        }
      }

      set
      {
        try
        {
          this.binaryContentData = Convert.FromBase64String(value);
        }
        catch (ArgumentNullException e)
        {
          throw e;
        }
        catch (FormatException)
        {
          throw new ArgumentException("Invalid Input String");
        }
      }
    }

    /// <summary>
    /// Gets or sets
    /// Extension for Syndication Items for GeoLocation Information
    /// </summary>
    [XmlIgnore]
    public SyndicationElementExtension GeoLocationInfo
    {
      get
      {
        return this.geoInfo;
      }

      set
      {
        this.geoInfo = value;
      }
    }

    /// <summary>
    /// Gets or sets
    /// XML Serialization Object for Uri
    /// </summary>
    [XmlElement(ElementName = "uri", Order = 3)]
    private string XmlUri
    {
      get
      {
        if (this.uri != null)
        {
          return this.URI.ToString();
        }
        else
        {
          return null;
        }
      }

      set
      {
        try
        {
          this.URI = new Uri(value);
        }
        catch (ArgumentNullException e)
        {
          throw e;
        }
        catch (FormatException)
        {
          throw new ArgumentException("Invalid Uri Format");
        }
      }
    }

    #endregion

    #region Public Member Functions

    /// <summary>
    /// Automatically Calculates the SHA-1 Hash of the Content Data and Stores it in the Digest as a Base64 String
    /// </summary>
    public void CalculateDigest()
    {
      if (this.binaryContentData == null)
      {
        throw new ArgumentNullException("BinaryContentData Can't Be Null to Compute The Hash");
      }

      SHA1 sha = new SHA1CryptoServiceProvider();
      byte[] result = sha.ComputeHash(this.binaryContentData);
      this.digest = Convert.ToBase64String(result);
    }

    /// <summary>
    /// Converts This Object To One or More Syndication Items
    /// </summary>
    /// <param name="items">Syndication Items with Associated GeoRSS Content</param>
    /// <param name="linkUID">Provides Capability to Link The ContentKey or UID with the SyndicationLink</param>
    public void ToGeoRSS(List<SyndicationItem> items, Guid linkUID)
    {
      if (items == (List<SyndicationItem>)null)
      {
        throw new ArgumentNullException("Input SyndicationItem List Can't Be Null");
      }

      SyndicationItem myitem = new SyndicationItem();
      if (items.Count == 0)
      {
        myitem.Authors.Add(new SyndicationPerson("admin@icnet.mitre.org", "Admin", String.Empty));
        myitem.Copyright = new TextSyndicationContent(EDXLConstants.SyndicationCopyright);
        myitem.Content = new TextSyndicationContent(this.mimeType);
        myitem.LastUpdatedTime = DateTime.UtcNow;
      }
      else if (items[0] != (SyndicationItem)null)
      {
        foreach (SyndicationPerson author in items[0].Authors)
        {
          myitem.Authors.Add(author);
        }

        foreach (SyndicationCategory cat in items[0].Categories)
        {
          myitem.Categories.Add(cat);
        }

        myitem.Copyright = new TextSyndicationContent(EDXLConstants.SyndicationCopyright);
        myitem.Content = new TextSyndicationContent(this.mimeType);
        myitem.LastUpdatedTime = items[0].LastUpdatedTime;
      }

      ////if the non XML content has a URI, use that for the syndication link
      if (this.uri != null)
      {
        myitem.Links.Add(new SyndicationLink(this.uri));
      }
      else
      {
        myitem.Links.Add(new SyndicationLink(new Uri(EDXLConstants.SyndicationLinkBaseURI + "MIME/" + linkUID.ToString())));
      }

      if (this.geoInfo != null)
      {
        myitem.ElementExtensions.Add(this.geoInfo);
      }
      else
      {
        TargetAreaType tatype = new TargetAreaType();
        tatype.AddCircle("0,0 0");

        StringBuilder output = new StringBuilder();
        XmlWriterSettings xsettings = new XmlWriterSettings();
        xsettings.Indent = true;
        xsettings.IndentChars = "\t";
        xsettings.OmitXmlDeclaration = true;
        XmlWriter xwriter = XmlWriter.Create(output, xsettings);
        xwriter.WriteStartElement(EDXLConstants.GeoRSSPrefix, "where", EDXLConstants.GeoRSSNamespace);
        tatype.ToGeoRSS(xwriter);
        xwriter.WriteEndElement();
        xwriter.Flush();
        xwriter.Close();
        MemoryStream ms = new MemoryStream(Encoding.ASCII.GetBytes(output.ToString()));
        myitem.ElementExtensions.Add(XmlReader.Create(ms));
      }

      myitem.PublishDate = DateTime.Now;
      myitem.Summary = new TextSyndicationContent("NonXMLContent");
      myitem.Title = new TextSyndicationContent(linkUID.ToString());
      items.Add(myitem);
    }

    /// <summary>
    /// Write This Object to an XML Document
    /// </summary>
    /// <param name="xwriter">XMLWriter to an existing XML Document</param>
    internal override void WriteXML(XmlWriter xwriter)
    {
      this.Validate();
      xwriter.WriteElementString("mimeType", this.mimeType);
      if (this.size != null)
      {
        xwriter.WriteElementString("size", this.size.ToString());
      }

      if (!String.IsNullOrEmpty(this.digest))
      {
        this.CheckDigest(this.digest);
        xwriter.WriteElementString("digest", this.digest);
      }

      if (this.uri != null)
      {
        xwriter.WriteElementString("uri", this.uri.ToString());
      }

      if (this.binaryContentData != null)
      {
        xwriter.WriteElementString("contentData", Convert.ToBase64String(this.binaryContentData));
      }
    }

    /// <summary>
    /// Reads This Object From an Existing XML Node
    /// </summary>
    /// <param name="rootnode">XMlNode that points to a nonxmlcontenttype</param>
    internal override void ReadXML(XmlNode rootnode)
    {
      foreach (XmlNode node in rootnode.ChildNodes)
      {
        if (String.IsNullOrEmpty(node.InnerText))
        {
          continue;
        }

        switch (node.LocalName)
        {
          case "mimeType":
            this.mimeType = node.InnerText;
            break;
          case "size":
            this.size = Int32.Parse(node.InnerText);
            break;
          case "digest":
            this.digest = node.InnerText;
            break;
          case "uri":
            this.uri = new Uri(node.InnerText);
            break;
          case "contentData":
            this.binaryContentData = Convert.FromBase64String(node.InnerText);
            break;
          case "#comment":
            break;
          default:
            throw new FormatException("Invalid value: " + node.Name + "found in ValueList Type");
        }
      }

      if (this.digest != null)
      {
        this.CheckDigest(this.digest);
      }

      this.Validate();
    }

    #endregion

    #region Private Member Functions
    /// <summary>
    /// Validates This Message Element For Required Values and Conformance
    /// </summary>
    protected override void Validate()
    {
      if (String.IsNullOrEmpty(this.mimeType))
      {
        throw new ArgumentNullException("MIME type is required and can't be null");
      }
    }

    /// <summary>
    /// Calculates and Verifies the SHA-1 Hash of the Binary Data
    /// </summary>
    /// <param name="s">Input Hash String to Validate</param>
    private void CheckDigest(string s)
    {
      if (this.binaryContentData == null)
      {
        throw new ArgumentNullException("BinaryContentData Can't Be Null to Compute The Hash");
      }

      SHA1 sha = new SHA1CryptoServiceProvider();
      byte[] result = sha.ComputeHash(this.binaryContentData);
      string temp = Convert.ToBase64String(result);
      if (temp != s)
      {
        throw new ArgumentException("Digest Value is incorrect");
      }
    }

    #endregion
  }
}
